Timer 제작 과정
setInterval → useInterval → Web Worker
TimeLog에 사용할 타이머를 만드는 과정에서 겪은 경험을 공유하고자 합니다.
처음은 누구라도 그러하듯이 setInterval로 타이머를 만들기 시작했습니다.
하지만 타이머가 제대로 작동하지 않아 검색을 해보니, setInterval에 몇 가지 문제가 있는데,
그 해결책으로 useInterval이라는 custom Hook을 발견했습니다. (문제에 관한 내용은 아래 참고 링크에 있습니다)
따라서 useInterval로 바꾸었고, 타이머가 작동하여 뿌듯한 기분으로 다른 작업을 하였습니다.
하지만 저의 뿌듯함을 비웃듯 타이머에 또 다른 문제가 생겼습니다.
타이머를 오래 작동하면 타이머에 지연이 생기는 것이었습니다.
이번에도 검색을 통해 문제의 원인을 찾아보았습니다.
- CPU가 과부하 상태인 경우
- 브라우저 탭이 백그라운드 모드인 경우
- 노트북이 배터리에 의존해서 구동 중인 경우
위의 경우 브라우저 내 타이머가 느려져 setInterval의 지연 간격이 길어진다는 것이었습니다. (useInterval도 setInterval을 사용합니다)
경우 추측
크롬은 메모리가 부족하면 비활성 탭이 절전 되는 기능인, “Automatic tab discarding”이 내장되어 있습니다.
“chrome://flags”에서 Automatic tab discarding을 조절할 수 있었지만 없어졌고,
“chrome://discards”에 접속하면 모든 탭에 Auto Discardable이 활성화 되어 있는 것을 확인할 수 있습니다.
(참고로 Graph탭에서 Web Workers를 확인할 수도 있습니다)
이 기능으로 인해 탭이 절전 됐거나, 다른 내장 기능에 의해 자동으로 백그라운드 모드에 진입해서 지연된 것 같습니다.
이를 해결하기 위한 무수한 검색 중에 Web Worker를 발견합니다.
Web Worker는 웹의 Main thread와 별개로, Background thread에서 script를 실행하는 기술입니다.
따라서 Web Worker script에서 setInterval을 사용하면 브라우저 내 타이머 지연과 상관없이 정상 작동합니다.
그러니 어서 사용합시다.
React & TypeScript
// tsconfig.json
{
"compilerOptions": {
...
"lib": [ ... "WebWorker"], // Worker Scope에서 쓰이는 타입 추가
...
}
}
// worker.ts
const self = globalThis as unknown as DedicatedWorkerGlobalScope; // Double assertion
let time = 0;
self.onmessage = () => {
setInterval(() => {
time++;
self.postMessage(time);
}, 100);
};
export {}; // --isolatedModules 에러 방지 (모듈화)
// Timer.tsx
function Timer() {
const [time, setTime] = useState(0);
const [timerOn, setTimerOn] = useState(false);
const timeSecond = Math.floor(time / 10);
const timerStart = () => {
setTimerOn(true);
};
const timerStop = () => {
setTimerOn(false);
};
useEffect(() => {
const worker = new Worker(new URL('worker/worker', import.meta.url)); // webpack5 이후 용법
if (timerOn === true) {
worker.postMessage('timer start');
worker.onmessage = (e: MessageEvent<string>) => {
setTime(time + Number(e.data));
};
}
return () => {
worker.terminate();
};
}, [timerOn]);
return (
<div>
<div className='timer'>{timeSecond}</div>
<div>
<button onClick={timerStart}>Start</button>
<button onClick={timerStop}>Stop</button>
</div>
</div>
);
}
export default Timer;
동작 과정
-
Background thread에서 실행할 Worker sciprt를 worker.ts에 작성합니다. (이하 worker)
-
Timer.tsx(main script)에서
new Worker()
로 worker를 실행합니다.- timerOn 값이 바뀔 때마다 그 전에
worker.terminate()
로 현재 worker가 종료되고, 이후 새로운 worker를 실행합니다.
- timerOn 값이 바뀔 때마다 그 전에
-
타이머 버튼의 클릭 이벤트로 timerOn 값이 true가 되면,
worker.postMessage()
로 worker에 메세지 데이터를 전달합니다.- 여기서 메세지 데이터는 사용하지 않으므로 중요하지 않습니다.
- 여기서 메세지 데이터는 사용하지 않으므로 중요하지 않습니다.
-
worker의
self.onmessage
에 적힌 동작들이 실행됩니다. (setInterval 실행) -
Timer.tsx에서
worker.onmessage
로, worker에서self.postMessage()
로 보낸 time을 100ms마다 기존 time에 추가합니다.